/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { ts, AstTreeUtils, MethodSubSignature, MethodSignature, ArkPtrInvokeExpr, FieldSignature, ArkMethod, ArkInstanceFieldRef, ArkInstanceInvokeExpr, Local, ArkInvokeStmt, Stmt, FunctionType, NumberType, UnknownType, ArkAssignStmt, ArkStaticFieldRef, ArkStaticInvokeExpr, ClassSignature, ArkNormalBinopExpr, ArkNewExpr } from "arkanalyzer";
import { MatcherCallback, MatcherTypes, MethodMatcher } from "../../matcher/Matchers";
import { Defects, IssueReport } from "../../model/Defects";
import { Rule } from "../../model/Rule";
import { BaseChecker, BaseMetaData } from "../BaseChecker";
import { RuleListUtil } from "../../utils/common/DefectsList";
import { Scene } from 'arkanalyzer';

const G_META_DATA: BaseMetaData = {
  severity: 2,
  ruleDocPath: 'docs/no-implied-eval-check.md',
  description: 'Implied eval. Consider passing a function.',
};
const errorFunctionMessage = 'Implied eval. Do not use the Function constructor to create functions.';
const FUNCTION_CONSTRUCTOR = 'Function';
const GLOBAL_CANDIDATES = new Set(['global', 'window', 'globalThis']);
const EVAL_LIKE_METHODS = new Set(['setImmediate', 'setInterval', 'setTimeout', 'execScript']);
interface WarnInfo {
  line: number;
  startCol: number;
  endCol: number;
  filePath: string;
};

export class NoImpliedEvalCheck implements BaseChecker {
  public defects: Defects[] = [];
  public issues: IssueReport[] = [];
  readonly metaData: BaseMetaData = G_META_DATA;
  public rule: Rule;
  private buildMatcher: MethodMatcher = {
    matcherType: MatcherTypes.METHOD,
  };

  public registerMatchers(): MatcherCallback[] {
    const matchBuildCallback: MatcherCallback = {
      matcher: this.buildMatcher,
      callback: this.check
    };
    return [matchBuildCallback];
  };

  public check = (arkMethod: ArkMethod): void => {
    const scene = arkMethod.getDeclaringArkFile().getScene();
    const statements = arkMethod.getBody()?.getCfg().getStmts() ?? [];
    for (const statement of statements) {
      if (statement instanceof ArkInvokeStmt) {
        const originalText = statement.getOriginalText();
        if (!originalText) { continue; }
        this.checkArkInvokeStmt(originalText, statement, scene);
      } else if (statement instanceof ArkAssignStmt) {
        const originalText = statement.getOriginalText();
        if (!originalText) { continue; };
        if (originalText.includes(FUNCTION_CONSTRUCTOR)) {
          const rightValue = statement.getRightOp();
          this.handleRightValue(statement, rightValue);
        };
      };
    };
  };

  private handleRightValue(statement: ArkAssignStmt, rightValue: any): void {
    if (rightValue instanceof ArkInstanceInvokeExpr) {
      const base = rightValue.getBase();
      if (base instanceof Local) {
        const baseType = base.getType();
        if (baseType instanceof FunctionType || baseType instanceof UnknownType) {
          this.addIssueReport(statement, base.getName(), errorFunctionMessage);
        };
      };
    } else if (rightValue instanceof ArkStaticInvokeExpr) {
      const methodName = rightValue.getMethodSignature()?.getMethodSubSignature()?.getMethodName();
      if (methodName) {
        this.addIssueReport(statement, methodName, errorFunctionMessage);
      };
    };
  };
  private checkArkInvokeStmt(originalText: string, statement: ArkInvokeStmt, scene: Scene): void {
    const filteredEvalLikeMethods = this.filterEvalLikeMethods(statement);
    const hasEvalLikeMethod = this.hasEvalLikeMethod(originalText, filteredEvalLikeMethods);
    const invokeExpression = statement.getInvokeExpr();

    if (hasEvalLikeMethod) {
      this.handleEvalLikeMethod(invokeExpression, originalText, statement, scene);
    } else if (originalText.includes(FUNCTION_CONSTRUCTOR)) {
      this.handleFunctionConstructor(invokeExpression, statement);
    }
  };
  private handleEvalLikeMethod(invokeExpression: any, originalText: string, statement: ArkInvokeStmt, scene: Scene): void {
    if (this.shouldSkipInvokeExpression(invokeExpression)) return;
    const argumentsList = invokeExpression.getArgs();
    if (this.hasInvalidArgsTypes(argumentsList, scene, originalText)) {
      let text = this.extractTextFromOriginal(originalText);
      const isGlobalCandidate = this.hasGlobalCandidate(originalText);
      if (isGlobalCandidate) {
        text = this.getValueAst(originalText);
      };
      const finalText = text !== undefined ? text : originalText;
      this.addIssueReport(statement, finalText, this.metaData.description);
    };
  };
  private extractTextFromOriginal(originalText: string): string {
    let regex = /(?:setTimeout|setInterval|setImmediate|execScript)\((.*?)(?:,|\))/;
    const match = originalText.match(regex);
    return match ? match[1].trim() : originalText;
  };
  private handleFunctionConstructor(invokeExpression: any, statement: ArkInvokeStmt): void {
    if (invokeExpression instanceof ArkInstanceInvokeExpr) {
      const baseType = invokeExpression.getBase()?.getType()?.getTypeString();
      if (baseType && this.isFunctionConstructor(baseType)) {
        const base = invokeExpression.getBase();
        if (base && base instanceof Local) {
          this.handleBaseDeclaringStatement(base, statement);
        };
      };
    };
  };
  private handleBaseDeclaringStatement(base: Local, statement: ArkInvokeStmt): void {
    const baseDeclaringStatement = base.getDeclaringStmt();
    if (baseDeclaringStatement && baseDeclaringStatement instanceof ArkAssignStmt) {
      const rightOperand = baseDeclaringStatement.getRightOp();
      if (rightOperand && rightOperand instanceof ArkNewExpr) {
        this.addIssueReport(statement, "new", errorFunctionMessage);
      };
    };
  };
  private filterEvalLikeMethods(statement: ArkInvokeStmt): Set<string> {
    const methods = new Set(EVAL_LIKE_METHODS);
    const file = statement.getCfg().getDeclaringMethod().getDeclaringArkClass().getDeclaringArkFile();
    file.getImportInfos().forEach(importInfo => {
      const importName = importInfo.getImportClauseName();
      if (methods.has(importName)) {
        methods.delete(importName);
      };
    });
    const classMethods = statement.getCfg().getDeclaringMethod().getDeclaringArkClass().getMethods();
    classMethods.forEach(method => {
      const methodName = method.getName();
      if (methods.has(methodName)) {
        methods.delete(methodName);
      };
    });
    return methods;
  };
  private hasGlobalCandidate(originalText: string): boolean {
    return Array.from(GLOBAL_CANDIDATES).some(method => originalText.includes(method));
  };
  private hasEvalLikeMethod(originalText: string, methods: Set<string>): boolean {
    return Array.from(methods).some(method => originalText.includes(method));
  };

  private shouldSkipInvokeExpression(invokeExpression: any): boolean {
    if (invokeExpression instanceof ArkInstanceInvokeExpr) {
      const base = invokeExpression.getBase();
      if (base instanceof Local) {
        const name = base.getName();
        const hasGlobalCandidate = Array.from(GLOBAL_CANDIDATES).some(method => name === method);
        return !hasGlobalCandidate;
      };
    };
    return false;
  };
  private hasInvalidArgsTypes(argumentsList: any[], scene: Scene, originalText: string): boolean {
    const argsMax = argumentsList.length;
    const leftType = argumentsList[0]?.getType();
    const rightType = argumentsList[1]?.getType();
    if (argsMax === 1 ? !(leftType instanceof FunctionType) :
      !(leftType instanceof FunctionType) || !(rightType instanceof NumberType)) {
      const leftTypeValue = leftType?.getTypeString();
      if (leftTypeValue === 'Function') {
        return false;
      };
      if (argumentsList[0] instanceof Local) {
        const leftValue = argumentsList[0];
        const declaringStatement = leftValue.getDeclaringStmt();
        if (declaringStatement instanceof ArkAssignStmt) {
          const declaringRightValue = declaringStatement.getRightOp();
          if (this.shouldSkipBasedOnBinopExpr(declaringRightValue)) {
            return false;
          };
        };
      };
      if (leftType instanceof UnknownType) {
        if (argumentsList[0] instanceof Local) {
          const leftValue = argumentsList[0];
          const declaringStatement = leftValue.getDeclaringStmt();
          if (declaringStatement instanceof ArkAssignStmt) {
            const declaringRightValue = declaringStatement.getRightOp();
            if (this.shouldSkipBasedOnStaticFieldRef(declaringRightValue)) {
              return false;
            };
            if (this.shouldSkipBasedOnInstanceInvokeExpr(declaringRightValue)) {
              return false;
            };
            if (this.shouldSkipBasedOnInstanceFieldRef(declaringRightValue, scene, originalText)) {
              return false;
            };
            if (this.shouldSkipBasedOnPtrInvokeExpr(declaringRightValue)) {
              return false;
            };
          };
        };
      };
      return true;
    };
    return false;
  };
  private shouldSkipBasedOnStaticFieldRef(declaringRightValue: any): boolean {
    if (declaringRightValue instanceof ArkStaticFieldRef) {
      const declaringRightType = declaringRightValue.getFieldSignature().getType();
      if (declaringRightType instanceof FunctionType) {
        return true;
      };
    };
    return false;
  };
  private shouldSkipBasedOnInstanceInvokeExpr(declaringRightValue: any): boolean {
    if (declaringRightValue instanceof ArkInstanceInvokeExpr) {
      const base = declaringRightValue.getBase();
      if (base instanceof Local) {
        const type = base.getType();
        if (type instanceof FunctionType) {
          return true;
        };
      };
    };
    return false;
  };

  private shouldSkipBasedOnInstanceFieldRef(declaringRightValue: any, scene: Scene, originalText: string): boolean {
    if (declaringRightValue instanceof ArkInstanceFieldRef) {
      let isFunction = false;
      const mFieldSignature = declaringRightValue.getFieldSignature();
      if (mFieldSignature instanceof FieldSignature) {
        const getDeclaringSignature = mFieldSignature.getDeclaringSignature();
        if (getDeclaringSignature instanceof ClassSignature) {
          const mClass = scene.getClass(getDeclaringSignature);
          const mFields = mClass?.getFields();
          if (mFields && mFields.length > 0) {
            for (const field of mFields) {
              const initializer = field.getInitializer();
              for (const minitializer of initializer) {
                if (minitializer instanceof ArkAssignStmt) {
                  const initializerRightValue = minitializer.getRightOp();
                  const type = initializerRightValue.getType();
                  if (type instanceof FunctionType) {
                    isFunction = true;
                    break;
                  };
                };
              };
              if (isFunction) {
                break;
              };
            };
          } else {
            const base = declaringRightValue.getBase();
            if (base && base instanceof Local) {
              const baseDeclaringStatement = base.getDeclaringStmt();
              if (baseDeclaringStatement instanceof ArkAssignStmt) {
                const baseDeclaringStmtText = baseDeclaringStatement.getOriginalText();
                if (baseDeclaringStmtText) {
                  const isFunctionType = this.checkTypeAst(baseDeclaringStmtText, originalText);
                  if (isFunctionType) {
                    return true;
                  };
                };
              };
            };
          };
        };
      }
      if (isFunction) {
        return true;
      };
    };
    return false;
  };

  private shouldSkipBasedOnPtrInvokeExpr(declaringRightValue: any): boolean {
    if (declaringRightValue instanceof ArkPtrInvokeExpr) {
      const funcPtrLocal = declaringRightValue.getFuncPtrLocal();
      if (funcPtrLocal instanceof Local) {
        const type = funcPtrLocal.getType();
        if (type instanceof FunctionType) {
          const methodSignature = type.getMethodSignature();
          if (methodSignature instanceof MethodSignature) {
            const getMethodSubSignature = methodSignature.getMethodSubSignature();
            if (getMethodSubSignature instanceof MethodSubSignature) {
              const returnType = getMethodSubSignature.getReturnType();
              if (returnType instanceof FunctionType) {
                return true;
              };
            };
          };
        };
      };
    };
    return false;
  };
  private shouldSkipBasedOnBinopExpr(declaringRightValue: any): boolean {
    if (declaringRightValue instanceof ArkNormalBinopExpr) {
      const rightOp1 = declaringRightValue.getOp1();
      const rightOp2 = declaringRightValue.getOp2();
      if (rightOp1 && rightOp2) {
        const type1 = rightOp1.getType();
        const type2 = rightOp2.getType();
        if (type1 instanceof FunctionType && type2 instanceof FunctionType) {
          return true;
        };
      };
    };
    return false;
  };
  private isFunctionConstructor(baseType: string): boolean {
    return baseType.includes('Function') || baseType.includes('window.Function') || baseType.includes('window[\'Function\']');
  };

  private checkTypeAst(baseDeclaringStmtText: string, originalText: string): boolean {
    const ast = AstTreeUtils.getASTNode('checkTypeAst.ts', baseDeclaringStmtText);
    const visit = (node: ts.Node): boolean => {
      if (ts.isMethodDeclaration(node)) {
        if (ts.isIdentifier(node.name)) {
          const name = node.name.text;
          if (originalText.includes(name)) {
            return true;
          };
        };
      };
      return ts.forEachChild(node, visit) || false;
    };
    return visit(ast);
  };
  private getValueAst(originalText: string): string {
    const ast = AstTreeUtils.getASTNode('getValueAst.ts', originalText);
    const visit = (node: ts.Node): string => {
      if (ts.isCallExpression(node)) {
        if (node.arguments.length > 0) {
          const text = node.arguments[0].getText();
          return text;
        };
      };
      const result = ts.forEachChild(node, visit);
      return result || originalText;
    };
    return visit(ast);
  };
  private addIssueReport(statement: Stmt, name: string, errorMessage: string): void {
    const warnInfo = this.getLineAndColumn(statement, name);
    const defect = new Defects(warnInfo.line, warnInfo.startCol, warnInfo.endCol, errorMessage, this.metaData.severity, this.rule.ruleId,
      warnInfo.filePath, this.metaData.ruleDocPath, true, false, false);
    this.issues.push(new IssueReport(defect, undefined));
    RuleListUtil.push(defect);
  };
  private getLineAndColumn(statement: Stmt, name: string): WarnInfo {
    const arkFile = statement.getCfg()?.getDeclaringMethod().getDeclaringArkClass().getDeclaringArkFile();
    if (arkFile) {
      const originText = statement.getOriginalText() ?? '';
      const pos = originText.indexOf(name);
      if (pos !== -1) {
        const originPosition = statement.getOriginPositionInfo();
        const line = originPosition.getLineNo();
        let startCol = originPosition.getColNo();
        startCol += pos;
        const endCol = startCol + name.length - 1;
        const originPath = arkFile.getFilePath();
        return { line, startCol, endCol, filePath: originPath };
      };
    };
    return { line: -1, startCol: -1, endCol: -1, filePath: '' };
  };
};